frontend/pages/e/[uuid]/details.tsx (view raw)
1import moment from 'moment';
2import Linkify from 'linkify-react';
3import Tooltip from '@mui/material/Tooltip';
4import IconButton from '@mui/material/IconButton';
5import Box from '@mui/material/Box';
6import Link from '@mui/material/Link';
7import Card from '@mui/material/Card';
8import Container from '@mui/material/Container';
9import TextField from '@mui/material/TextField';
10import Typography from '@mui/material/Typography';
11import TuneIcon from '@mui/icons-material/Tune';
12import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
13import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
14import {useTheme} from '@mui/material/styles';
15import {DatePicker} from '@mui/x-date-pickers/DatePicker';
16import {PropsWithChildren, useState} from 'react';
17import {useTranslation} from 'next-i18next';
18import pageUtils from '../../../lib/pageUtils';
19import DetailsLink from '../../../containers/DetailsLink';
20import ShareEvent from '../../../containers/ShareEvent';
21import PlaceInput from '../../../containers/PlaceInput';
22import LangSelector from '../../../components/LangSelector';
23import usePermissions from '../../../hooks/usePermissions';
24import useEventStore from '../../../stores/useEventStore';
25import useToastStore from '../../../stores/useToastStore';
26import EventLayout, {TabComponent} from '../../../layouts/Event';
27import {
28 EventByUuidDocument,
29 useUpdateEventMutation,
30} from '../../../generated/graphql';
31import {langLocales} from '../../../locales/constants';
32import {getLocaleForLang} from '../../../lib/getLocale';
33
34interface Props {
35 eventUUID: string;
36 announcement?: string;
37}
38
39const Page = (props: PropsWithChildren<Props>) => {
40 return <EventLayout {...props} Tab={DetailsTab} />;
41};
42
43const DetailsTab: TabComponent<Props> = ({}) => {
44 const {t} = useTranslation();
45 const {
46 userPermissions: {canEditEventDetails},
47 } = usePermissions();
48 const theme = useTheme();
49 const [updateEvent] = useUpdateEventMutation();
50 const addToast = useToastStore(s => s.addToast);
51 const setEventUpdate = useEventStore(s => s.setEventUpdate);
52 const event = useEventStore(s => s.event);
53 const [isEditing, setIsEditing] = useState(false);
54
55 if (!event) return null;
56
57 const hasGeoloc = event.latitude && event.longitude;
58
59 const onSave = async e => {
60 try {
61 const {uuid, ...data} = event;
62 const {
63 id,
64 travels,
65 waitingPassengers,
66 __typename,
67 administrators,
68 passengers,
69 ...input
70 } = data;
71 await updateEvent({
72 variables: {
73 uuid,
74 eventUpdate: {
75 ...input,
76 },
77 },
78 refetchQueries: ['eventByUUID'],
79 });
80 setIsEditing(false);
81 } catch (error) {
82 console.error(error);
83 addToast(t('event.errors.cant_update'));
84 }
85 };
86
87 const modifyButton = isEditing ? (
88 <Tooltip
89 title={t('event.details.save')}
90 sx={{
91 position: 'absolute',
92 top: theme.spacing(2),
93 right: theme.spacing(2),
94 }}
95 >
96 <IconButton color="primary" onClick={onSave}>
97 <CheckCircleOutlineIcon />
98 </IconButton>
99 </Tooltip>
100 ) : (
101 <Tooltip
102 title={t('event.details.modify')}
103 sx={{
104 position: 'absolute',
105 top: theme.spacing(2),
106 right: theme.spacing(2),
107 }}
108 >
109 <IconButton color="primary" onClick={() => setIsEditing(true)}>
110 <TuneIcon />
111 </IconButton>
112 </Tooltip>
113 );
114
115 return (
116 <Box
117 sx={{
118 position: 'relative',
119 }}
120 >
121 <Container
122 sx={{
123 p: 4,
124 mt: 6,
125 mb: 11,
126 mx: 0,
127 [theme.breakpoints.down('md')]: {
128 p: 2,
129 },
130 }}
131 >
132 <Card
133 sx={{
134 position: 'relative',
135 maxWidth: '100%',
136 width: '480px',
137 p: 2,
138 }}
139 >
140 <Typography variant="h4" pb={2}>
141 {t('event.details')}
142 </Typography>
143 {canEditEventDetails() && modifyButton}
144 {(isEditing || event.name) && (
145 <Box pt={2} pr={1.5}>
146 <Typography variant="overline">
147 {t('event.fields.name')}
148 </Typography>
149 <Typography>
150 {isEditing ? (
151 <TextField
152 size="small"
153 fullWidth
154 value={event.name}
155 onChange={e => setEventUpdate({name: e.target.value})}
156 name="name"
157 id="EditEventName"
158 />
159 ) : (
160 <Typography id="EventName">{event.name}</Typography>
161 )}
162 </Typography>
163 </Box>
164 )}
165 {(isEditing || event.date) && (
166 <Box pt={2} pr={1.5}>
167 <Typography variant="overline">
168 {t('event.fields.date')}
169 </Typography>
170 {isEditing ? (
171 <Typography>
172 <DatePicker
173 slotProps={{
174 textField: {
175 size: 'small',
176 id: `EditEventDate`,
177 fullWidth: true,
178 placeholder: t('event.fields.date_placeholder'),
179 },
180 }}
181 format="DD/MM/YYYY"
182 value={moment(event.date)}
183 onChange={date =>
184 setEventUpdate({
185 date: !date ? null : moment(date).format('YYYY-MM-DD'),
186 })
187 }
188 />
189 </Typography>
190 ) : (
191 <Box position="relative">
192 <Typography id="EventDate">
193 {moment(event.date).format('DD/MM/YYYY')}
194 </Typography>
195 </Box>
196 )}
197 </Box>
198 )}
199 {(isEditing || event.address) && (
200 <Box pt={2} pr={1.5}>
201 <Typography variant="overline">
202 {t('event.fields.address')}
203 </Typography>
204 {isEditing ? (
205 <PlaceInput
206 place={event.address}
207 latitude={event.latitude}
208 longitude={event.longitude}
209 onSelect={({place, latitude, longitude}) =>
210 setEventUpdate({
211 address: place,
212 latitude,
213 longitude,
214 })
215 }
216 />
217 ) : (
218 <Box position="relative">
219 <Typography
220 id="EventAddress"
221 title={t`placeInput.noCoordinates`}
222 sx={{
223 pr: 3,
224 display: 'inline-flex',
225 alignItems: 'center',
226 columnGap: 1,
227 }}
228 >
229 <Link
230 target="_blank"
231 rel="noreferrer"
232 href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
233 event.address
234 )}`}
235 onClick={e => e.preventDefault}
236 >
237 {event.address}
238 </Link>
239 {!hasGeoloc && (
240 <InfoOutlinedIcon fontSize="small" color="warning" />
241 )}
242 </Typography>
243 </Box>
244 )}
245 </Box>
246 )}
247 {(isEditing || event.description) && (
248 <Box pt={2} pr={1.5}>
249 <Typography variant="overline">
250 {t('event.fields.description')}
251 </Typography>
252 {isEditing ? (
253 <Typography>
254 <TextField
255 fullWidth
256 multiline
257 maxRows={4}
258 inputProps={{maxLength: 250}}
259 value={event.description || ''}
260 onChange={e =>
261 setEventUpdate({description: e.target.value})
262 }
263 id={`EditEventDescription`}
264 name="description"
265 />
266 </Typography>
267 ) : (
268 <Typography
269 id="EventDescription"
270 sx={{pr: 3, whiteSpace: 'pre-line'}}
271 >
272 <Linkify options={{render: DetailsLink}}>
273 {event.description}
274 </Linkify>
275 </Typography>
276 )}
277 </Box>
278 )}
279 {(isEditing || event.lang) && (
280 <Box pt={2} pr={1.5}>
281 <Typography variant="overline">
282 {t('event.fields.lang')}
283 </Typography>
284 {isEditing ? (
285 <LangSelector
286 value={event.lang}
287 onChange={lang => setEventUpdate({lang})}
288 />
289 ) : (
290 <Typography id="EventLang" sx={{pr: 3}}>
291 {langLocales[event.lang]}
292 </Typography>
293 )}
294 </Box>
295 )}
296 {!isEditing && (
297 <ShareEvent
298 title={`Caroster ${event.name}`}
299 sx={{width: '100%', mt: 2}}
300 />
301 )}
302 </Card>
303 </Container>
304 </Box>
305 );
306};
307
308export const getServerSideProps = pageUtils.getServerSideProps(
309 async (context, apolloClient) => {
310 const {uuid} = context.query;
311 const {host = ''} = context.req.headers;
312 let event = null;
313
314 // Fetch event
315 try {
316 const {data} = await apolloClient.query({
317 query: EventByUuidDocument,
318 variables: {uuid},
319 });
320 event = data?.eventByUUID?.data;
321 } catch (error) {
322 return {
323 notFound: true,
324 };
325 }
326
327 const description = await getLocaleForLang(
328 event?.attributes?.lang,
329 'meta.description'
330 );
331
332 return {
333 props: {
334 eventUUID: uuid,
335 metas: {
336 title: event?.attributes?.name || '',
337 description,
338 url: `https://${host}${context.resolvedUrl}`,
339 },
340 },
341 };
342 }
343);
344export default Page;